package ru.ncedu.java.dmsi.robots.world;

import ru.ncedu.java.dmsi.robots.actions.Action;
import ru.ncedu.java.dmsi.robots.actions.impl.MakeShotAction;
import ru.ncedu.java.dmsi.robots.actions.impl.MoveAction;
import ru.ncedu.java.dmsi.robots.model.*;
import ru.ncedu.java.dmsi.robots.strategies.impl.BorderGuardMoveStrategy;

import java.util.*;

public class World {
    private int widthOfWorld, heightOfWorld;
    private Map<Integer, ActiveWorldObject> activeObjects; // Key is robot's id
    private ArrayList<ArrayList<StatePoint>> field;

    private boolean isPointOccupy( Point p ) {
        if (p.getX() < 0 || p.getX() >= widthOfWorld || p.getY() < 0 || p.getY() >= heightOfWorld)
            return true;
        return field.get(p.getX()).get(p.getY()).isOccupy();
    }
    public World( int width, int height, int numOfRobots, int numOfWall ) {
        widthOfWorld = width;
        heightOfWorld = height;

        // Reducing number of robots (it cannot be more then points in the world)
        if (numOfRobots > width * height)
            numOfRobots = width * height;
        field = new ArrayList<ArrayList<StatePoint>>(height);
        for (int i = 0; i < height; i++) {
            field.add(new ArrayList<StatePoint>(width));
        }
        for (int i = 0; i < heightOfWorld; i++) {
            for (int j = 0; j < widthOfWorld; j++) {
                field.get(i).add(new StatePoint(i, j, false));
            }
        }
        activeObjects = new HashMap<Integer, ActiveWorldObject>(numOfRobots);

        Random generator = new Random(System.currentTimeMillis());

        // Initialize robots (random)
        PointFactory pf = PointFactory.getInstance();
        Point p;
        Robot r;
        for (int i = 0; i < numOfRobots; i++) {
            p = pf.getPoint(generator.nextInt(widthOfWorld), generator.nextInt(heightOfWorld));
            // Check by occupying of initializing point
            while (isPointOccupy(p)) {
                p = pf.getPoint(generator.nextInt(widthOfWorld), generator.nextInt(heightOfWorld));
            }
            r = new Robot(p, new BorderGuardMoveStrategy());
            activeObjects.put(r.getId(), r);
            field.get(p.getX()).get(p.getY()).occupy(r);
        }
        // Initialize walls
        for (int i = 0; i < numOfWall; i++) {
            p = new Point(generator.nextInt(widthOfWorld), generator.nextInt(heightOfWorld));
            while (isPointOccupy(p)) {
                p = pf.getPoint(generator.nextInt(widthOfWorld), generator.nextInt(heightOfWorld));
            }
            field.get(p.getX()).get(p.getY()).occupy(new Wall(p));
        }

    }

    public void doStep() {
        Action action;
        Set<Integer> removeSet = new HashSet<Integer>();
        Set<Bullet> bulletSet = new HashSet<Bullet>();
        for (Map.Entry<Integer, ActiveWorldObject> entry : activeObjects.entrySet()) {
            ActiveWorldObject actor = entry.getValue();
            Point entryLocation = actor.getPoint();
            // Check by correct location
            if (actor == field.get(entryLocation.getX()).get(entryLocation.getY()).getOccupant()) {
                action = actor.doStep(new ActionContext(this, entryLocation));

                if (actor instanceof Robot) {
                    // Check returned point by correct
                    if (action instanceof MoveAction)
                        removeSet.addAll(robotMoveActionHandler((MoveAction)action, entryLocation));
                    else if (action instanceof MakeShotAction)
                        bulletSet.add(robotShotActionHandler((MakeShotAction)action, entryLocation));
                }
                else if (actor instanceof Bullet) {
                    removeSet.addAll(bulletMoveActionHandler((MoveAction) action, entryLocation));
                }
            }
        }
        for (Bullet bullet : bulletSet) {
            Point loc = bullet.getPoint();
            Action act = bullet.doStep(new ActionContext(this, loc));
            WorldObject wo = null;
            if (field.get(loc.getX()).get(loc.getY()).isOccupy())
                wo = field.get(loc.getX()).get(loc.getY()).getOccupant();
            field.get(loc.getX()).get(loc.getY()).occupy(bullet);
            activeObjects.put(bullet.getId(), bullet);
            removeSet.addAll(bulletMoveActionHandler((MoveAction)act, bullet.getPoint()));
            if (wo != null)
                field.get(loc.getX()).get(loc.getY()).occupy(wo);
        }
        for (Integer id : removeSet)
            activeObjects.remove(id);
    }
    public void doSteps( int numOfSteps ) {
        for (int i = 0; i < numOfSteps; i++)
            doStep();
    }

    public boolean addRobot( Robot robot ) {
        if (robot == null)
            return false;
        Point location = robot.getPoint();
        if (isPointOccupy(location))
            return false;
        field.get(location.getX()).get(location.getY()).occupy(robot);
        activeObjects.put(robot.getId(), robot);
        return true;
    }
    public boolean addRobotAnywhere( Robot robot ) {
        if (addRobot(robot))
            return true;
        PointFactory pf = PointFactory.getInstance();
        Point location = null;

        // Search free point
        boolean find = false;
        for (int i = getMinXCoord(); i < getMaxXCoord() && !find; i++)
            for (int j = getMinYCoord(); j < getMaxYCoord() && !find; j++) {
                location = pf.getPoint(i, j);
                find = isPointFree(location);
            }
        if (!find)
            return false; // Have not free point
        robot.move(location);
        field.get(location.getX()).get(location.getY()).occupy(robot);
        activeObjects.put(robot.getId(), robot);
        return true;
    }

    public boolean addWall( Wall wall ) {
        Point location = wall.getPoint();
        if (isPointOccupy(location))
            return false;
        field.get(location.getX()).get(location.getY()).occupy(wall);
        return true;
    }

    private Set<Integer> bulletMoveActionHandler( MoveAction action, Point oldLoc ) {
        Point newLocation = action.getPoint();
        int dist = Math.abs(newLocation.getX() - oldLoc.getX()) + Math.abs(newLocation.getY() - oldLoc.getY());
        if (dist != 2)
            throw new RuntimeException("Too high bullet speed"); // And what exception type accord for this situation?
        Bullet bullet; // current handled bullet
        WorldObject bulletObject = field.get(oldLoc.getX()).get(oldLoc.getY()).getOccupant();
        if (bulletObject instanceof Bullet)
            bullet = (Bullet) bulletObject;
        else
            throw new RuntimeException("Incorrect field occupying for bullets");

        Set<Integer> removeSet = new HashSet<Integer>();
        if (isOutOfWorld(newLocation)) {
            removeSet.add(bullet.getId());
            field.get(oldLoc.getX()).get(oldLoc.getY()).free();
            return removeSet;
        }

        if (isPointOccupy(newLocation)) { // Potential collision
            WorldObject wo = field.get(newLocation.getX()).get(newLocation.getY()).getOccupant();

            if (wo instanceof Bullet) {
                if (wo == bullet)
                    throw new RuntimeException("Bullet " + bullet.getId() + " do not move"); // Robot do not move. Do no destroy it
                field.get(newLocation.getX()).get(newLocation.getY()).free();
                removeSet.add(((Bullet) wo).getId()); // Robots collision
            }
            else if (wo instanceof Robot) {
                // Collision with robot - destroy robot
                field.get(newLocation.getX()).get(newLocation.getY()).occupy(bullet);
                removeSet.add(((Robot) wo).getId());
            }
            field.get(oldLoc.getX()).get(oldLoc.getY()).free();
            removeSet.add(bullet.getId()); // Destroy current bullet because of colli
        }
        else {
            // No collision - relocation bullet
            field.get(oldLoc.getX()).get(oldLoc.getY()).free();
            field.get(newLocation.getX()).get(newLocation.getY()).occupy(bullet);
            bullet.move(newLocation);
        }
        return removeSet;
    }
    private Set<Integer> robotMoveActionHandler( MoveAction action, Point oldLoc ) {
        Point newLocation = action.getPoint();
        if (Math.abs(newLocation.getX() - oldLoc.getX()) + Math.abs(newLocation.getY() - oldLoc.getY()) > 1)
            throw new RuntimeException(); // And what exception type accord for this situation?
        Robot robot; // current handled robot
        WorldObject robotObject = field.get(oldLoc.getX()).get(oldLoc.getY()).getOccupant();
        if (robotObject instanceof Robot)
            robot = (Robot) robotObject;
        else
            throw new RuntimeException("Incorrect field occupying");

        Set<Integer> removeSet = new HashSet<Integer>();
        if (isOutOfWorld(newLocation)) {
            removeSet.add(robot.getId());
            field.get(oldLoc.getX()).get(oldLoc.getY()).free();
            return removeSet;
        }

        if (isPointOccupy(newLocation)) { // Potential collision
            WorldObject wo = field.get(newLocation.getX()).get(newLocation.getY()).getOccupant();

            if (wo instanceof Robot) {
                if (wo == robot)
                    return null; // Robot do not move. Do no destroy it
                field.get(newLocation.getX()).get(newLocation.getY()).free();
                removeSet.add(((Robot) wo).getId()); // Robots collision
            }
            else if (wo instanceof Bullet) {
                // Collision with bullet - destroy bullet
                field.get(newLocation.getX()).get(newLocation.getY()).free();
                removeSet.add(((Bullet) wo).getId());
            }
            field.get(oldLoc.getX()).get(oldLoc.getY()).free();
            removeSet.add(robot.getId()); // Destroy current robot because of colli
        }
        else {
            field.get(oldLoc.getX()).get(oldLoc.getY()).free();
            field.get(newLocation.getX()).get(newLocation.getY()).occupy(robot);
            robot.move(newLocation);
        }
        return removeSet;
    }
    private Bullet robotShotActionHandler( MakeShotAction action, Point location ) {
        return new Bullet(location, action.getDirection());
    }

    public void Out() {
        for (Map.Entry<Integer, ActiveWorldObject> entry : activeObjects.entrySet()) {
            if (entry.getValue() instanceof Robot)
                System.out.println("Robot #");
            else
                System.out.println("Bullet #");
            System.out.println(entry.getValue().getId() +
                    " in place: (" + entry.getValue().getPoint().getX() + ", " +
                    entry.getValue().getPoint().getY() + ")");
        }
    }
    public void Out2() {
        for (ArrayList<StatePoint> row : field) {
            for (StatePoint p : row) {
                if (p.isOccupy())
                    System.out.print(p.getOccupant().toString());
                else
                    System.out.print(".");
            }
            System.out.println();
        }
    }
    public boolean isPointFree( Point p ) {
        return !isPointOccupy(p);
    }
    public boolean isOutOfWorld( Point p ) {
        return (p.getX() < 0) || (p.getX() >= widthOfWorld) ||
               (p.getY() < 0) || (p.getY() >= heightOfWorld);
    }
    public int getMinXCoord() {
        return 0;
    }
    public int getMaxXCoord() {
        return widthOfWorld - 1;
    }
    public int getMinYCoord() {
        return 0;
    }
    public int getMaxYCoord() {
        return heightOfWorld - 1;
    }
}
